Skip to content

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439

Open
secengjeff wants to merge 16 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open
Open

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439
secengjeff wants to merge 16 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open

Conversation

@secengjeff
Copy link
Copy Markdown

@secengjeff secengjeff commented Mar 26, 2026

Replaces the legacy MacOSXOSAScript class (which constructs and executes AppleScript via osascript) with a new MacOS class that uses Apple’s purpose-built /usr/bin/open utility.

The old MacOSXOSAScript class is now deprecated and emits a DeprecationWarning on use.

Motivation

This change is discussed in:

The previous PATH-lookup vulnerability with osascript was addressed in a separate PR. However, use of osascript remains a concern for the following reasons:

Usability Risk
On managed enterprise Macs, EDR/MDM tools (CrowdStrike, SentinelOne, Jamf, Microsoft Defender for Endpoint, etc.) often monitor or restrict osascript due to its abuse in malware and supply-chain attacks. When restrictions apply, webbrowser.open() can fail silently or with unclear errors, breaking a simple, commonly used API.

Security Risk
osascript is a general-purpose scripting interpreter and a classic Living-Off-the-Land binary (LOOBin). It was used on macOS in the Axios npm supply-chain attack (March 31, 2026). Even after the PATH fix, it still requires constructing and executing AppleScript — a more powerful mechanism than needed for the simple task of opening a URL. This unnecessarily increases the standard library’s attack surface and keeps Python dependent on a tool that security teams frequently treat with caution.

Benefits of the New Implementation

  • Calls /usr/bin/open directly (absolute path) via subprocess.run with a safe argument list, no shell and no scripting interpreter.
  • For known browsers, uses open -b <bundle-id> (e.g., com.google.Chrome) to explicitly target the intended application.
  • Safely routes non-HTTP(S) URLs through a browser to avoid unintended OS file-handler behavior.
  • Improves default browser detection using plistlib.
  • Maintains full backward compatibility, including support for named browsers (webbrowser.get("chrome"), etc.).
  • Simplifies the code and reduces reliance on a general-purpose LOOBin.

Changes

  • Lib/webbrowser.py: New MacOS class, deprecation of MacOSXOSAScript, updated browser registration.
  • Lib/test/test_webbrowser.py: Tests for the new implementation and deprecation warning.
  • Doc/library/webbrowser.rst: Documentation updates and deprecation note.

This is a low-risk modernization that improves both reliability in enterprise environments and the overall security posture of the webbrowser module.

References

…ate MacOSXOSAScript

Add a new MacOSX class that opens URLs via subprocess.run(['/usr/bin/open', ...])
instead of piping AppleScript to osascript. For named browsers, /usr/bin/open -a
<name> is used; for the default browser, /usr/bin/open <url> defers directly to
the OS URL handler.

MacOSXOSAScript is deprecated with a DeprecationWarning pointing users to MacOSX.
register_standard_browsers() is updated to use MacOSX for all macOS registrations.

osascript is a general-purpose scripting interpreter that is routinely blocked on
managed endpoints due to its abuse potential, causing webbrowser.open() to fail
silently. /usr/bin/open is Apple's purpose-built URL-opening primitive and carries
no such restrictions. This also eliminates the PATH-injection vector in the existing
os.popen("osascript", "w") call.
…pt deprecation

Add MacOSXTest covering default browser open, named browser open, and failure
case (non-zero returncode). Add MacOSXOSAScriptDeprecationTest verifying that
instantiating MacOSXOSAScript emits a DeprecationWarning. All tests mock
subprocess.run.
@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Mar 26, 2026

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@python-cla-bot
Copy link
Copy Markdown

python-cla-bot bot commented Mar 26, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

@secengjeff secengjeff changed the title gh-137586: Replace MacOSXOSAScript with MacOSX on macOS, using /usr/bin/open gh-137586: Replace 'osascript' with 'open' on macOS Mar 26, 2026
- Add test_default to MacOSXTest asserting webbrowser.get() returns MacOSX
- Remove test_default from MacOSXOSAScriptTest (no longer the registered default)
- Suppress DeprecationWarning in MacOSXOSAScriptTest setUp and test_explicit_browser
  using warnings.catch_warnings() so tests for OSAScript behaviour still run cleanly
- Add warnings import
…ia OS handler

For non-http(s) URLs (e.g. file://), /usr/bin/open dispatches via the OS
file handler, which would launch an .app bundle rather than open it in a
browser. Fix this by routing non-http(s) URLs through the browser explicitly
using /usr/bin/open -b <bundle-id>.

Named browsers use a static bundle ID map (Chrome, Firefox, Safari, Chromium,
Opera, Edge). Unknown named browsers fall back to -a. For the default browser,
the bundle ID is resolved at runtime via the Objective-C runtime using
NSWorkspace.URLForApplicationToOpenURL, the same lookup MacOSXOSAScript
performed via AppleScript. Falls back to direct open if ctypes is unavailable.

http/https URLs with the default browser continue to use /usr/bin/open
directly, as macOS always routes these to the registered browser.
…ult_browser_bundle_id

NSWorkspace is an AppKit class and is not registered in the ObjC runtime
until AppKit is loaded. Without the explicit LoadLibrary call, objc_getClass
returns nil for NSWorkspace, causing the entire lookup to silently fall back
to /usr/bin/open without -b.
@vstinner vstinner changed the title gh-137586: Replace 'osascript' with 'open' on macOS gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser Mar 27, 2026
(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.15

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. deprecated:: 3.14
.. deprecated:: next

And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?

@gpshead gpshead requested a review from ned-deily April 5, 2026 22:51
@ned-deily ned-deily requested review from a team and ronaldoussoren April 6, 2026 00:19
Copy link
Copy Markdown
Contributor

@ronaldoussoren ronaldoussoren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this PR.

The direct security issue can be fixed by changing the invocation of osascript to /usr/bin/osascript. Blocking osascript while allowing usage of Python is IMHO security theatre.

On modernist systems (macOS 10.15 or later) it is probably possible to just use NSWorkspace directly: it has all the moving peaces to implement what we need for webbrowser.open although I haven't thought through the implications of doing this yet. In particular, -[NSWorkpace openURLs:withApplicationAtURL:configuration:completionHandler:] is asynchronous which makes it harder to report errors.

#

if sys.platform == 'darwin':
def _macos_default_browser_bundle_id():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function leaks memory due to not performing objective-c reference count updates.

Also: This introduces a new dependency on an Apple system framework, which in the past has caused problems for folks starting new worker processes using os.fork (without exec-in a new executable).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that hardcoding /usr/bin/osascript addresses the PATH injection issue directly. This PR is intended as an incremental improvement on top of that.

The core motivation is that osascript is increasingly blocked at the EDR/MDM level in enterprise environments as a broad response to ClickFix campaigns and supply chain attacks like the axios incident. /usr/bin/open is both incrementally safer as a purpose-built URL dispatch primitive and far less likely to be subject to those same restrictions. When osascript is blocked, users of Python-based tools may see a failure with no obvious connection to osascript, and the path to diagnosing an endpoint security policy conflict is not straightforward for most users.

On _macos_default_browser_bundle_id(): agreed, I'll fix the missing Objective-C reference count cleanup and address the os.fork() concern.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up replacing the AppKit/ctypes implementation with a plistlib read of the LaunchServices preferences file to address the memory and forking risks.

@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Apr 6, 2026

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. deprecated:: 3.14
.. deprecated:: next

And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.

(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?

'firefox': 'org.mozilla.firefox',
'safari': 'com.apple.Safari',
'chromium': 'org.chromium.Chromium',
'opera': 'com.operasoftware.Opera',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have Opera installed on macOS, and confirm this is the correct bundle ID in the plist, but whilst this works:

import webbrowser
web = webbrowser.get("firefox")
web.open("https://www.python.org/")

This doesn't:

import webbrowser
web = webbrowser.get("opera")
web.open("https://www.python.org/")
Traceback (most recent call last):
  File "/Users/hugo/github/python/cpython/main/1.py", line 2, in <module>
    web = webbrowser.get("opera")
  File "/Users/hugo/github/python/cpython/main/Lib/webbrowser.py", line 68, in get
    raise Error("could not locate runnable browser")
webbrowser.Error: could not locate runnable browser

Do these new ones need registering too, or removing from here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for catching this. I registered the browsers and fixed the Edge bundle. Tested both Opera and Edge locally.

@merwok merwok added type-feature A feature request or enhancement OS-mac labels Apr 6, 2026
secengjeff and others added 4 commits April 6, 2026 09:22
…_standard_browsers on macOS

These browsers were present in MacOSX._BUNDLE_IDS but not registered,
causing webbrowser.get("opera") etc. to raise Error: could not locate
runnable browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@secengjeff
Copy link
Copy Markdown
Author

I have made the requested changes; please review again

@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Apr 6, 2026

Thanks for making the requested changes!

@ronaldoussoren: please review the changes made to this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting change review OS-mac type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants